/*
 * Copyright 2015-2018 Igor Maznitsa.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mindolph.mindmap.extension.exporters;

import com.igormaznitsa.mindmap.model.MindMap;
import com.mindolph.base.FontIconManager;
import com.mindolph.base.constant.IconKey;
import com.mindolph.base.constant.StrokeType;
import com.mindolph.base.graphic.Graphics;
import com.mindolph.base.util.GeometryConvertUtils;
import com.mindolph.mfx.util.AwtConvertUtils;
import com.mindolph.mfx.util.FontUtils;
import com.mindolph.mfx.util.FxImageUtils;
import com.mindolph.mindmap.I18n;
import com.mindolph.mindmap.MindMapConfig;
import com.mindolph.mindmap.MindMapContext;
import com.mindolph.mindmap.extension.api.BaseExportExtension;
import com.mindolph.mindmap.extension.api.ExtensionContext;
import com.mindolph.mindmap.gfx.MindMapCanvas;
import com.mindolph.mindmap.model.TopicNode;
import com.mindolph.mindmap.theme.MindMapTheme;
import com.mindolph.mindmap.util.DialogUtils;
import com.mindolph.mindmap.util.MindMapUtils;
import javafx.geometry.Dimension2D;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.image.Image;
import javafx.scene.input.ClipboardContent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.shape.*;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.List;
import java.util.*;

import static com.mindolph.mindmap.MindMapCalculateHelper.calculateSizeOfMapInPixels;

public class SVGImageExporter extends BaseExportExtension {

    protected static final String FONT_CLASS_NAME = "mindMapTitleFont";
    private static final Map<String, String[]> LOCAL_FONT_MAP = new HashMap<>() {
        {
            put("dialog", new String[]{"sans-serif", "SansSerif"});
            put("dialoginput", new String[]{"monospace", "Monospace"});
            put("monospaced", new String[]{"monospace", "Monospace"});
            put("serif", new String[]{"serif", "Serif"});
            put("sansserif", new String[]{"sans-serif", "SansSerif"});
            put("symbol", new String[]{"'WingDings'", "WingDings"});
        }
    };
    private static final Logger LOGGER = LoggerFactory.getLogger(SVGImageExporter.class);
    private static final String SVG_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<!-- Generated by Mindolph SVG exporter https://github.com/mindolph/Mindolph -->\n<svg version=\"1.1\" baseProfile=\"tiny\" id=\"svg-root\" width=\"%d%%\" height=\"%d%%\" viewBox=\"0 0 %s %s\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">";
    private static final String NEXT_LINE = "\n";
    private static final DecimalFormat DOUBLE;
    private boolean flagExpandAllNodes = false;
    private boolean flagDrawBackground = true;

    static {
        DOUBLE = new DecimalFormat("#.###", DecimalFormatSymbols.getInstance(Locale.US));
    }

    private static String dbl2str(double value) {
        return DOUBLE.format(value);
    }

    private static String fontFamilyToSVG(Font font) {
        String fontFamilyStr = font.getFamily();
        String[] logicalFontFamily = LOCAL_FONT_MAP.get(font.getName().toLowerCase());
        if (logicalFontFamily != null) {
            fontFamilyStr = logicalFontFamily[0];
        }
        else {
            fontFamilyStr = String.format("'%s'", fontFamilyStr);
        }
        return fontFamilyStr;
    }

    private static String font2style(Font font) {
        StringBuilder result = new StringBuilder();
        FontWeight weight = FontUtils.fontWeight(font.getStyle());
        FontPosture posture = FontUtils.fontPosture(font.getStyle());
        String fontStyle = posture == FontPosture.ITALIC ? "italic" : "normal";
        String fontWeight = weight == FontWeight.BOLD ? "bold" : "normal";
        String fontSize = DOUBLE.format(font.getSize()) + "px";
        String fontFamily = fontFamilyToSVG(font);

        result.append("font-family: ").append(fontFamily).append(';').append(NEXT_LINE);
        result.append("font-size: ").append(fontSize).append(';').append(NEXT_LINE);
        result.append("font-style: ").append(fontStyle).append(';').append(NEXT_LINE);
        result.append("font-weight: ").append(fontWeight).append(';').append(NEXT_LINE);

        return result.toString();
    }

    @Override
    public List<String> getOptions() {
        return Arrays.asList(I18n.getIns().getString("SvgExporter.optionUnfoldAll"), I18n.getIns().getString("SvgExporter.optionDrawBackground"));
    }

    @Override
    public List<Boolean> getDefaults() {
        return Arrays.asList(false, true);
    }


    private String makeContent(ExtensionContext context, List<Boolean> options) throws IOException {
        if (options != null) {
            this.flagExpandAllNodes = options.get(0);
            this.flagDrawBackground = options.get(1);
        }
        MindMap<TopicNode> workMap = new MindMap<>(context.getModel());
        workMap.resetPayload();

        if (this.flagExpandAllNodes) {
            workMap.getRoot().removeCollapseAttr();
        }

        MindMapConfig newConfig = new MindMapConfig(context.getMindMapConfig());
        MindMapTheme theme = newConfig.getTheme();
        String[] mappedFont = LOCAL_FONT_MAP.get(newConfig.getTopicFont().getFamily().toLowerCase(Locale.ENGLISH));
        if (mappedFont != null) {
            FontWeight weight = FontUtils.fontWeight(newConfig.getTopicFont().getStyle());
            FontPosture posture = FontUtils.fontPosture(newConfig.getTopicFont().getStyle());
            Font adaptedFont = Font.font(mappedFont[1], weight, posture, newConfig.getTopicFont().getSize());
            newConfig.setTopicFont(adaptedFont);
        }

        theme.setDrawBackground(this.flagDrawBackground);

        MindMapContext mindMapContext = new MindMapContext();
        Dimension2D blockSize = calculateSizeOfMapInPixels(workMap, newConfig, mindMapContext, flagExpandAllNodes);
        if (blockSize == null) {
            return SVG_HEADER + "</svg>";
        }

        StringBuilder buffer = new StringBuilder(16384);
        buffer.append(String.format(SVG_HEADER, 100, 100, dbl2str(blockSize.getWidth()), dbl2str(blockSize.getHeight()))).append(NEXT_LINE);
        buffer.append(prepareStylePart(buffer, newConfig)).append(NEXT_LINE);

        BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        Graphics gfx = new SVGMMGraphics(buffer, g);

        gfx.setClip(0, 0, Math.round(blockSize.getWidth()), Math.round(blockSize.getHeight()));
        try {
            MindMapCanvas mindMapCanvas = new MindMapCanvas(gfx, newConfig, mindMapContext);
            mindMapCanvas.layoutFullDiagramWithCenteringToPaper(workMap, GeometryConvertUtils.dimension2DToBounds(blockSize));
            mindMapCanvas.drawOnGraphicsForConfiguration(workMap, false, null);
        } finally {
            gfx.dispose();
        }
        buffer.append("</svg>");

        return buffer.toString();
    }

    @Override
    public void doExportToClipboard(ExtensionContext context, List<Boolean> options) throws IOException {
        String text = makeContent(context, options);
        ClipboardContent cc = new ClipboardContent();
        cc.putString(text);
        javafx.scene.input.Clipboard.getSystemClipboard().setContent(cc);
    }

    @Override
    public void doExport(ExtensionContext context, List<Boolean> options, String exportFileName, OutputStream out) throws IOException {
        String text = makeContent(context, options);

        File fileToSave = null;
        if (out == null) {
            fileToSave = DialogUtils.selectFileToSaveForFileFilter(
                    I18n.getIns().getString("SvgExporter.saveDialogTitle"), null,
                    ".svg",
                    I18n.getIns().getString("SvgExporter.filterDescription"),
                    exportFileName);
            fileToSave = MindMapUtils.checkFileAndExtension(fileToSave, ".svg");
            out = fileToSave == null ? null : new BufferedOutputStream(new FileOutputStream(fileToSave, false));
        }
        if (out != null) {
            try {
                IOUtils.write(text, out, "UTF-8");
                Files.setLastModifiedTime(fileToSave.toPath(), FileTime.fromMillis(System.currentTimeMillis()));
            } finally {
                if (fileToSave != null) {
                    IOUtils.closeQuietly(out);
                }
            }
        }
    }


    private String prepareStylePart(StringBuilder buffer, MindMapConfig config) {
        return """
                <style>
                @font-face {
                    font-family: 'fontawesomeregular';
                    src: url(data:application/font-woff2;charset=utf-8;base64,d09GMgABAAAAAAaEABAAAAAADbwAAAYnAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGh4GYACDSggCCYRlEQgKgkyDBgteAAE2AiQDgRwEIAWGZweCdQwuG3gMUVSQSgG+OLDp9Aus0EgBMcaDeXzWHBCAbKs15G9l9sLilsjT4B/y7/PcefPlhdxGHchO7a7rQF1agusucSX8r039FwBSQLLsLJYJUwgASCWEK9Gabp36foC20npFXIau7ZNVm97gEZLJCvX4qNIdKlV9WXpBHNxUjnSQNAytlCdH+G0/tX25/8YKWNGNjEKnlsb/XstvRDcg1ATx00BqafwAwc0DCTWhmhmxTOs9pC3ZBdoraOPzSwECeHN59y2Au32LvADQ/9ChERAEUgAMQlRjYAEDOK6Luo0L62qwFrD5bjWgJV0B2HPl7tsPB/5XKZ3/OgHtcQYQd8qbIWcpgiy69Voh4fMksQsogZKEmxB5jLObAxzjBryiGbSWrmY2W7VtjeOfTp+/ojXwOu+KDZ4d+n8r/n/Ta+pVepke0c060Jf15+2fhd/fu/90n/2el/0xtCTgwqAqkRuBRMkAzj8gsMDSeT3pvpRMf2YGTIALeMQhqg6RGIWj+yWOPKSQwiFl6HZOX6zmn/ykoh/RKKwpc6FE3nfMclajiRNdg7KFK+bCRLG1jGJ9v2RYr1yYJMTXYZwdk7+IpQhvqfxPPg1KOcNkYhC2HlJqlVbmmryDyF6xzIUpYsJmXIkk0isrVhAEVY2pyOzO9a3TREE+jXkG5emCnjR04VfEUhQTHR2cYlJtJ0L/dHOjuZZCxyhhTBFzhg3qjSZleFibR/KYKhWZgr4PyZI1uIIHU50rJKVtvHXtNirphnVqBK8t27puS6Mmi8ZM4+aAuEkbN5CO9ZiuyBj6bRs3nRzlWFt38ecyxgj9bHYNyktTB0UY8Y8Ya1Ou4PSzA3tzKruGCENDSRNtbQcXikbNDpOvNaCKwGx5TwUUWPIH7P1YBUHYzKuFb6PWRCO20kJBzbvtcupad27gZipil4vIM+qhRWIK6lXf2Gh0zeeBpScEMkYkrDMo+TqKwBvJpjFKFcX6+fUcM0keTW2cFBV0x7z1mL6x0p5MFkiRHOmIN2ao/msirAZmgxeG4lIxt/gjeVBpXRgczmat4p4iX2qgOf/ortzmHNF8T++OuPYYb3DVI0x81ZPGxNCz8cNviW07npUNP3nmRzmtMJGwebav6ehc6tij3KwzuD8mz99+GsTVCVBMCjX/a+aD1Hxx238M0zrtne+Y2L94+mjeyitPnlzhqr1bI3ZoitSlWOr0xgF4kt/zCPgPPPPUbz398GucdIIAAEeSACoPxSzkHky3sNKD1hIWeZAXWGRhUd5EsuE8lG1hVd5EhuE85LMOOwzn4Yyq/AIsiMFEYdHgx2fHvD4wAKu7F26ekqtzB35t54UvMrX835XSWV8Y8GIAELg7/99AvMB4MgK4Sp4FOgA8gNkDBkyIN9NZpsvcb2NGgOhEYUUS6ASYxxYIem8dHmaAWLTqBQDUUIOdxCINgm4pgCDPLSnKQfwloN005g4gP0vm8W5EFonI+oC5ggMXlmZugPaYjp/FchdFnOduDDu5hy3s5l7S1MV9JGk9D1ChwzzMQD3mtyBFv/htqNNffgfCphDuKvXtXzuQYZa2/3N2fsPhjT8VGf9rMn9s47rv2f76aThrtDa9TCI1As20GYE2Ws3ZAD9efIRxQ2lQKkGShpuVfaDQIB1V02mkF8WRg0yjBoK4ieDGq1ueFu0FpPUYNLK4ARzQ9GwpdVz8FnIl1mhFlcbGUGgTjcZZFdpqDlX94F4BNMLCjDFpgl1ihmx0UtFaWF/QYJm3NjA+s0ap3p6tPR48PozWsqSsDRF3UcgOj6iO2n7IGDIRki+zgkLu2QwmC+uHoXiJEsePv1fkJZL/Aj+BjEBcMCm9MTxjPWkyuOmXQ7pZa0aHfhYljyqp5uPcKvzCcljHalaxlg1sVJJccssjr3zyK6CgQgoroijX2cR65atAhSpSsUpUqjKVq0JElapStWpExbjvObhrq9frbe67NXh9/V4v+tCPAQxiCMMYwSjGlAYPf6sSUtc2TUnzQsHYTiGrnq3+FtFHPwYwiCEMWyIKfMKxKMawARuxCZsv1jIvfRGNG4x6b7+ckLFuxsTQb4fyMKqm05MGB6Udqgpn282WCpOc) format('woff2');
                    font-weight: normal;
                    font-style: normal;
                
                }
                @font-face {
                    font-family: 'material_iconsregular';
                    src: url(data:application/font-woff2;charset=utf-8;base64,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) format('woff2');
                    font-weight: normal;
                    font-style: normal;
                
                }
                
                
                @font-face {
                    font-family: 'material_design_iconsregular';
                    src: url(data:application/font-woff2;charset=utf-8;base64,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) format('woff2');
                    font-weight: normal;
                    font-style: normal;
                
                }
                @font-face {
                    font-family: 'octiconsregular';
                    src: url(data:application/font-woff2;charset=utf-8;base64,d09GMgABAAAAAAUMABAAAAAAChAAAAStAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGh4GYACCaggCCYRlEQgKghyCMgsoAAE2AiQDTAQgBYJ8B4FODC4b4gjIngV2a8cxiUlDacOQ+p1XXIyH5/ernfu+zMyKS0LqSkJcQsKngUdK2DSERrQo1giN0Nbv06V/DUAKjCKbJRPgroHPQEGukqcQVCmapJo7hzqgjqkouaP8A29nDLhtYBpQ167W1P7N/4ZRIQoXpyNUbkNQAhS2tQ4IVGNraxSg6/hK1cF54yUlh/RnhYC/RzaeAvh2+fA5gH8Xc75BoAV9IAmhi5AjIS+uidPwlBfLnheA6FV2zn6gL4GkqFEcHSrh647cqpvGIQT+V2wDShbqEMDCCQIZKozElNzIB3tHHvaANlVJqXXCjzJoLOHPNwQgCi/leXz5+sX5PjL4HATNL0pCLJGQ+e8E8IAGzIbw/dzaKbl5h2IYazFcpskyla74fkUvWYSnx1gcJr3ngDSLk9nSLesMpVucmfMxxOudUud1ZXFuNltDN/Q98pP2SGl1uuVfbS+equYi08z1O9StlYqxuDADhwctLk3nlDjaKZmOXhjWmFrlisjO8CvXr5phoVSUYXHNpP0DnPu+9kgRZwlNBnGe2MzoPGM77Any5hZquq40O8Z142DFppkNh2ghukJ9zSY9X8rlWkzK4ErasCRqMOonFkjS9GRzrJNvHZXSarKLGHvMsCfs6aHyRsc1XMnWKC0knXkHpfXr2sztW+t1jK5r9Npu6ZbigiZifWc6TVSZaDcNej3w7gbJlh5N51DSZo/EJqOyF9l7bWMiwaJG5XzHEZCwbKfHPl+ta/J5W8oadMX4KpB2d5tkK/nnW1bdPC03V5i4llLtdu6BPeaqyqisrhYtN0MwxcaKtSclXOOYNCaJGZsaZaJbEa+n13KB1FyvdgkScDHTFMc61ozT0mtyTovBrD+XBSG2JRh0A+ZAgQmwG0COl3CjPW5IeR4d3v6PgndPBvq2RDw67B+o5cFed/9U99tj752dCfWama6E50nw69+qOPTjUCpQh6c6HIenIwLklie7V145n5oK4Lud3xoDWJ8HZEl2FW4R6STcUpz1kJ8mDgKVsshlsk5P7940uIJguVF55PIOalpeXff2rS1zmcHZIw7LFhydwV6XsCCew5UQZ1Uoy5sfcXi2c0CkxYa9O8NWO5SZkfHRRXB6wWFXZ0RUkfLDM7MgEPR+uN86Flz622e9D05qBhGlEqo/BoLS9ltgcQsoCfCLu6uwDrgCVWQoXZKsUQfCAh/JDu4k5mPPmiq2btHcfjcJSR8ElwF0eAgBdlUSF5IhQn1u4OhSGOQu8pqQGwKOhLHoTJ/LHYUxGxylZN3xCVGqHZ+RSTq+IMgL+1d4eb/OH3cMuXRw49S257q09Xf/1gHH5Oui/yDSmXv3c9hiq3kbrSCZjDVLukxaRgbaJs0wHdsQgzmR00gth6k2kg+HcxXLdimTz0nRUJkp+2bTHGZM2G7JNkscNqrvr6XH5lzLvC22vOISN2DbAydple4GIUWKLPIoooxKVKMWTdEcLdEabdEeHSLUe/vKfGZmZq3LZEMmZEE25EAu5EE+FEAhFHFVkux6Li9OamsCP1Nl681yrpvi/1icQEvDp1M6hXinnPUn+kzuUC0c0S1319VaTs0alooj0gAAAAA=) format('woff2');
                    font-weight: normal;
                    font-style: normal;
                
                }
                .FontAwesome {
                    font-family: 'fontawesomeregular';
                }
                .MaterialIcons {
                    font-family: 'material_iconsregular';
                }
                .MaterialDesignIcons {
                    font-family: 'material_design_iconsregular';
                }
                .octicons {
                    font-family: 'octiconsregular';
                }
                .%s {
                %s}
                </style>
                """.formatted(FONT_CLASS_NAME, font2style(config.getNoteFont()));
    }

    @Override

    public String getName(ExtensionContext context, TopicNode actionTopic) {
        return I18n.getIns().getString("SvgExporter.exporterName");
    }

    @Override
    public String getReference(ExtensionContext context, TopicNode actionTopic) {
        return I18n.getIns().getString("SvgExporter.exporterReference");
    }

    @Override
    public Text getIcon(ExtensionContext panel, TopicNode actionTopic) {
        return FontIconManager.getIns().getIcon(IconKey.IMAGE);
    }

    @Override
    public int getOrder() {
        return 5;
    }

    @Override
    public boolean needsTopicUnderMouse() {
        return false;
    }

    public static class SvgClip implements Transferable {

        private static final DataFlavor SVG_FLAVOR = new DataFlavor("image/svg+xml; class=java.io.InputStream", "Scalable Vector Graphic");
        final private String svgContent;

        private final DataFlavor[] supportedFlavors;

        public SvgClip(String str) {
            this.supportedFlavors = new DataFlavor[]{
                    SVG_FLAVOR,};

            this.svgContent = str;
            SystemFlavorMap systemFlavorMap = (SystemFlavorMap) SystemFlavorMap.getDefaultFlavorMap();
            DataFlavor dataFlavor = SVG_FLAVOR;
            systemFlavorMap.addUnencodedNativeForFlavor(dataFlavor, "image/svg+xml");
        }


        static DataFlavor getSVGFlavor() {
            return SvgClip.SVG_FLAVOR;
        }

        @Override
        public boolean isDataFlavorSupported(DataFlavor flavor) {
            for (DataFlavor supported : this.supportedFlavors) {
                if (flavor.equals(supported)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public DataFlavor[] getTransferDataFlavors() {
            return this.supportedFlavors;
        }

        @Override
        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
            if (isDataFlavorSupported(flavor) && flavor.equals(SVG_FLAVOR)) {
                return new ByteArrayInputStream(this.svgContent.getBytes(StandardCharsets.UTF_8));
            }
            throw new UnsupportedFlavorException(flavor);
        }

        public void lostOwnership(Clipboard clipboard, Transferable tr) {
        }
    }

    private static final class SVGMMGraphics implements Graphics {

        private static final DecimalFormat OPACITY = new DecimalFormat("#.##");
        private final StringBuilder buffer;
        private final Graphics2D context;
        private double translateX;
        private double translateY;
        private float strokeWidth = 1.0f;
        private StrokeType strokeType = StrokeType.SOLID;

        private SVGMMGraphics(StringBuilder buffer, Graphics2D context) {
            this.buffer = buffer;
            this.context = (Graphics2D) context.create();
        }


        private static String svgRgb(Color color) {
            return String.format("rgb(%s,%s,%s)", color.getRed() * 255, color.getGreen() * 255, color.getBlue() * 255);
        }

        private void printFillOpacity(Color color) {
            if (color.getOpacity() < 1) {
                this.buffer.append(" fill-opacity=\"").append(OPACITY.format(color.getOpacity())).append("\" ");
            }
        }

        private void printStrokeData(Color color) {
            this.buffer.append(" stroke=\"").append(svgRgb(color))
                    .append("\" stroke-width=\"").append(dbl2str(this.strokeWidth)).append("\"");

            switch (this.strokeType) {
                case SOLID:
                    this.buffer.append(" stroke-linecap=\"round\"");
                    break;
                case DASHES:
                    this.buffer.append(" stroke-linecap=\"butt\" stroke-dasharray=\"").append(dbl2str(this.strokeWidth * 3.0f)).append(',').append(dbl2str(this.strokeWidth)).append("\"");
                    break;
                case DOTS:
                    this.buffer.append(" stroke-linecap=\"butt\" stroke-dasharray=\"").append(dbl2str(this.strokeWidth)).append(',').append(dbl2str(this.strokeWidth * 2.0f)).append("\"");
                    break;
            }
        }

        @Override
        public double getFontMaxAscent() {
            return this.context.getFontMetrics().getMaxAscent();
        }

        @Override
        public Rectangle2D getStringBounds(String str) {
            if (str.isEmpty()) {
                return AwtConvertUtils.awtRectangle2D2Rectangle2D(this.context.getFontMetrics().getStringBounds("", this.context));
            }
            else {
                TextLayout textLayout = new TextLayout(str, this.context.getFont(), this.context.getFontRenderContext());
                return new Rectangle2D(0, -textLayout.getAscent(), textLayout.getAdvance(), textLayout.getAscent() + textLayout.getDescent() + textLayout.getLeading());
            }
        }

        @Override
        public void setClip(double x, double y, double w, double h) {
            this.context.setClip((int) x, (int) y, (int) w, (int) h);
        }

        @Override
        public Graphics copy() {
            SVGMMGraphics result = new SVGMMGraphics(this.buffer, this.context);
            result.translateX = this.translateX;
            result.translateY = this.translateY;
            result.strokeType = this.strokeType;
            result.strokeWidth = this.strokeWidth;
            return result;
        }

        @Override
        public void dispose() {
            this.context.dispose();
        }

        @Override
        public void translate(double x, double y) {
            this.translateX += x;
            this.translateY += y;
            this.context.translate(x, y);
        }

        @Override
        public void setClipBounds(Rectangle2D clipBounds) {

        }

        @Override
        public Rectangle2D getClipBounds() {
            return AwtConvertUtils.awtRectangle2D2Rectangle2D(this.context.getClipBounds());
        }

        @Override
        public void setStroke(float width, StrokeType type) {
            if (type != this.strokeType || Float.compare(this.strokeWidth, width) != 0) {
                this.strokeType = type;
                this.strokeWidth = width;
                if (Float.compare(this.strokeWidth, width) != 0) {
                    this.strokeType = type;
                    this.strokeWidth = width;

                    Stroke stroke = switch (type) {
                        case SOLID -> new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER);
                        case DASHES ->
                                new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f, new float[]{width * 2.0f, width}, 0.0f);
                        case DOTS ->
                                new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{width, width * 2.0f}, 0.0f);
                        default -> throw new Error("Unexpected stroke type : " + type);
                    };

                    this.context.setStroke(stroke);
                }
            }
        }

        @Override
        public void drawLine(Point2D start, Point2D end, Color color) {
            this.drawLine(start.getX(), start.getY(), end.getX(), end.getY(), color);
        }

        @Override
        public void drawLine(double startX, double startY, double endX, double endY, Color color) {
            this.buffer.append("<line x1=\"").append(dbl2str(startX + this.translateX))
                    .append("\" y1=\"").append(dbl2str(startY + this.translateY))
                    .append("\" x2=\"").append(dbl2str(endX + this.translateX))
                    .append("\" y2=\"").append(dbl2str(endY + this.translateY)).append("\" ");
            if (color != null) {
                printStrokeData(color);
                printFillOpacity(color);
            }
            this.buffer.append("/>").append(NEXT_LINE);
        }

        @Override
        public void drawString(String text, double x, double y, Color color) {
            this.buffer.append("<text x=\"").append(dbl2str(this.translateX + x)).append("\" y=\"").append(dbl2str(this.translateY + y)).append('\"');
            if (color != null) {
                this.buffer.append(" fill=\"").append(svgRgb(color)).append("\"");
                printFillOpacity(color);
            }
            this.buffer.append(' ');
            this.buffer.append("class=\"").append(FONT_CLASS_NAME).append('\"');
            this.buffer.append('>').append(StringEscapeUtils.escapeXml10(text)).append("</text>").append(NEXT_LINE);
        }

        @Override
        public void drawFontIcon(Font font, String iconText, double x, double y, Color fill) {
            this.setFont(font);
            this.buffer.append("<text x=\"").append(dbl2str(this.translateX + x)).append("\" y=\"").append(dbl2str(this.translateY + y)).append('\"');
            if (fill != null) {
                this.buffer.append(" fill=\"").append(svgRgb(fill)).append("\"");
                printFillOpacity(fill);
            }
            this.buffer.append(' ');
            this.buffer.append("class=\"").append(StringUtils.deleteWhitespace(this.context.getFont().getName())).append('\"');
            String icon = "&#" + (int)iconText.charAt(0) + ";";
            this.buffer.append('>').append(icon).append("</text>").append(NEXT_LINE);
        }

        @Override
        public void drawRect(double x, double y, double width, double height, Color border, Color fill) {
            this.buffer.append("<rect x=\"").append(dbl2str(this.translateX + x))
                    .append("\" y=\"").append(dbl2str(translateY + y))
                    .append("\" width=\"").append(dbl2str(width))
                    .append("\" height=\"").append(dbl2str(height))
                    .append("\" ");
            if (border != null) {
                printStrokeData(border);
            }

            if (fill == null) {
                this.buffer.append(" fill=\"none\"");
            }
            else {
                this.buffer.append(" fill=\"").append(svgRgb(fill)).append("\"");
                printFillOpacity(fill);
            }

            this.buffer.append("/>").append(NEXT_LINE);
        }

        @Override
        public void drawRect(Rectangle2D rect, Color border, Color fill) {
            this.drawRect(rect.getMinX(), rect.getMinY(), rect.getWidth(), rect.getHeight(), border, fill);
        }

        @Override
        public void draw(Shape shape, Color border, Color fill) {
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle) shape;
                this.buffer.append("<rect x=\"").append(dbl2str(this.translateX + rect.getX()))
                        .append("\" y=\"").append(dbl2str(translateY + rect.getY()))
                        .append("\" width=\"").append(dbl2str(rect.getWidth()))
                        .append("\" height=\"").append(dbl2str(rect.getHeight()))
                        .append("\" rx=\"").append(dbl2str(rect.getArcWidth() / 2.0d))
                        .append("\" ry=\"").append(dbl2str(rect.getArcHeight() / 2.0d))
                        .append("\" ");

            }
            else if (shape instanceof Path path) {
                double[] data = new double[6];
                this.buffer.append("<path d=\"");
                boolean nofirst = false;
                for (PathElement e : path.getElements()) {
                    if (nofirst) {
                        this.buffer.append(' ');
                    }
                    switch (e) {
                        case MoveTo moveTo ->
                                this.buffer.append("M ").append(dbl2str(this.translateX + moveTo.getX())).append(' ').append(dbl2str(this.translateY + moveTo.getY()));
                        case LineTo lineTo ->
                                this.buffer.append("L ").append(dbl2str(this.translateX + lineTo.getX())).append(' ').append(dbl2str(this.translateY + lineTo.getY()));
                        case CubicCurveTo cubicCurveTo ->
                            // todo the order of the control points should be tested.
                                this.buffer.append("C ")
                                        .append(dbl2str(this.translateX + cubicCurveTo.getX())).append(' ').append(dbl2str(this.translateY + cubicCurveTo.getY())).append(',')
                                        .append(dbl2str(this.translateX + cubicCurveTo.getControlX1())).append(' ').append(dbl2str(this.translateY + cubicCurveTo.getControlY1())).append(',')
                                        .append(dbl2str(this.translateX + cubicCurveTo.getControlX2())).append(' ').append(dbl2str(this.translateY + cubicCurveTo.getControlY2()));
                        case QuadCurveTo quadCurveTo ->
                            // todo the order of the control points should be tested.
                                this.buffer.append("Q ")
                                        .append(dbl2str(this.translateX + quadCurveTo.getX())).append(' ').append(dbl2str(this.translateY + quadCurveTo.getY())).append(',')
                                        .append(dbl2str(this.translateX + quadCurveTo.getControlX())).append(' ').append(dbl2str(this.translateY + quadCurveTo.getControlY()));
                        case ClosePath closePath -> this.buffer.append("Z");
                        case null, default -> LOGGER.warn("Unexpected path segment type");
                    }
                    nofirst = true;
                }
                this.buffer.append("\" ");
            }
            else {
                LOGGER.warn("Detected unexpected shape : " + shape.getClass().getName());
            }

            if (border != null) {
                printStrokeData(border);
            }

            if (fill == null) {
                this.buffer.append(" fill=\"none\"");
            }
            else {
                this.buffer.append(" fill=\"").append(svgRgb(fill)).append("\"");
                printFillOpacity(fill);
            }

            this.buffer.append("/>").append(NEXT_LINE);
        }

        @Override
        public void drawCurve(double startX, double startY, double endX, double endY, Color color) {
            this.buffer.append("<path d=\"M").append(dbl2str(startX + this.translateX)).append(',').append(startY + this.translateY)
                    .append(" C").append(dbl2str(startX))
                    .append(',').append(dbl2str(endY))
                    .append(' ').append(dbl2str(startX))
                    .append(',').append(dbl2str(endY))
                    .append(' ').append(dbl2str(endX))
                    .append(',').append(dbl2str(endY))
                    .append("\" fill=\"none\"");

            if (color != null) {
                printStrokeData(color);
            }
            this.buffer.append(" />").append(NEXT_LINE);
        }

        @Override
        public void drawBezier(double startX, double startY, double endX, double endY, Color color) {
            String s = """
                    <path d="M %s %s Q %s %s, %s %s T %s %s" 
                    """;
            double c1x = startX + (endX - startX) / 2;
            double c1y = startY;
            double c2x = startX + (endX - startX) / 2;
            double c2y = startY + (endY - startY) / 2;
            String formatted = s.formatted(
                    dbl2str(startX), dbl2str(startY),
                    dbl2str(c1x), dbl2str(c1y),
                    dbl2str(c2x), dbl2str(c2y),
                    dbl2str(endX), dbl2str(endY)
            );
            this.buffer.append(formatted);
            if (color != null) {
                printStrokeData(color);
            }
            this.buffer.append(" fill=\"none\"/>").append(NEXT_LINE);
        }

        @Override
        public void drawOval(double x, double y, double w, double h, Color border, Color fill) {
            double rx = w / 2.0d;
            double ry = h / 2.0d;
            double cx = x + this.translateX + rx;
            double cy = y + this.translateY + ry;

            this.buffer.append("<ellipse cx=\"").append(dbl2str(cx))
                    .append("\" cy=\"").append(dbl2str(cy))
                    .append("\" rx=\"").append(dbl2str(rx))
                    .append("\" ry=\"").append(dbl2str(ry))
                    .append("\" ");

            if (border != null) {
                printStrokeData(border);
            }

            if (fill == null) {
                this.buffer.append(" fill=\"none\"");
            }
            else {
                this.buffer.append(" fill=\"").append(svgRgb(fill)).append("\"");
                printFillOpacity(fill);
            }

            this.buffer.append("/>").append(NEXT_LINE);
        }

        @Override
        public void drawImage(Image image, double x, double y) {
            this.drawImage(image, x, y, image.getWidth(), image.getHeight());
        }

        @Override
        public void drawImage(Image image, double x, double y, double width, double height) {
            if (image != null) {
                try {
                    String s = FxImageUtils.imageToBase64(image);
                    this.buffer.append("<image width=\"").append(width).append("\" height=\"").append(height).append("\" x=\"").append(dbl2str(this.translateX + x)).append("\" y=\"").append(dbl2str(this.translateY + y)).append("\" xlink:href=\"data:image/png;base64,");
                    this.buffer.append(s);
                    this.buffer.append("\"/>").append(NEXT_LINE);
                } catch (Exception ex) {
                    LOGGER.error("Can't place image for error", ex);
                }
            }
        }

        @Override
        public void setFont(Font font) {
            this.context.setFont(FontUtils.fxFontToAwtFont(font));
        }

        @Override
        public void setOpacity(double opacity) {
            // TODO
        }
    }
}
